/**
 *
 */
package com.angel.data.generator.base.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.log4j.Logger;

import com.angel.dao.generic.factory.DAOFactory;
import com.angel.data.generator.base.DataClassGenerator;
import com.angel.data.generator.base.DataGeneratorRunner;
import com.angel.data.generator.checkers.DataGeneratorChecker;
import com.angel.data.generator.checkers.DataGeneratorCheckerRunner;
import com.angel.data.generator.checkers.impl.InverseDependenceDataGeneratorChecker;
import com.angel.data.generator.checkers.impl.NullDataGeneratorChecker;
import com.angel.data.generator.exceptions.DataGeneratorCheckerException;
import com.angel.data.generator.exceptions.DataGeneratorException;
import com.angel.data.generator.interfaces.DataGenerator;
import com.angel.data.generator.preProcess.PreProcessStrategy;
import com.angel.data.generator.repository.DataGeneratorRepository;
import com.angel.data.generator.result.DataGeneratorResult;




/**
 *	@author William
 *	@since 15/January/2009.
 */
public class DataGeneratorRunnerImpl implements DataGeneratorRunner {

	private static final long serialVersionUID = 7111376235641523647L;

	/**
     * Logger class.
     */
    private static Logger LOGGER = Logger.getLogger(DataGeneratorRunnerImpl.class);

    /**
     * Repository for all data generator classes.
     */
    private DataGeneratorRepository dataGeneratorRepository;
    /**
     * Check runner for check all data generator classes to add to this process.
     */
    private DataGeneratorCheckerRunner checkerRunner = new DataGeneratorCheckerRunner();

    private PreProcessStrategy preProcessStrategy;

    private Boolean preProcessExecuted = Boolean.FALSE;

    /**
     * DAO factory to use when data have to be generated.
     */
    private DAOFactory daoFactory;

    public DataGeneratorRunnerImpl(){
        super();
        this.dataGeneratorRepository = new DataGeneratorRepository();
        this.checkerRunner.addChecker(new NullDataGeneratorChecker());
        this.checkerRunner.addChecker(new InverseDependenceDataGeneratorChecker());
    }

    public DataGeneratorRunnerImpl(DAOFactory daoFactory){
        this();
        this.daoFactory = daoFactory;
        this.checkerRunner.addChecker(new NullDataGeneratorChecker());
        this.checkerRunner.addChecker(new InverseDependenceDataGeneratorChecker());
    }

    public DataGeneratorRunnerImpl(DAOFactory daoFactory, PreProcessStrategy preProcessStrategy){
        this();
        this.daoFactory = daoFactory;
        this.checkerRunner.addChecker(new NullDataGeneratorChecker());
        this.checkerRunner.addChecker(new InverseDependenceDataGeneratorChecker());
        this.preProcessStrategy = preProcessStrategy;
    }

    /**
     * @return the checkerRunner
     */
    public DataGeneratorCheckerRunner getCheckerRunner() {
        return checkerRunner;
    }

    public void addDataGeneratorChecker(DataGeneratorChecker checker){
    	this.getCheckerRunner().addChecker(checker);
    }

    public void addDataGeneratorCheckers(Collection<DataGeneratorChecker> checkers){
    	this.getCheckerRunner().addAllChecker(checkers);
    }

    public void removeDataGeneratorChecker(DataGeneratorChecker checker){
    	this.getCheckerRunner().removeChecker(checker);
    }

    public void containsDataGeneratorChecker(DataGeneratorChecker checker){
    	this.getCheckerRunner().containsChecker(checker);
    }

    /**
     * @return the daoFactory
     */
    public DAOFactory getDaoFactory() {
        return daoFactory;
    }

    public void addDataGeneratorClass(Class<? extends DataGenerator> dataGeneratorClass){
        dataGeneratorRepository.putDependenciesFor(dataGeneratorClass);
    }

    public void addDataGenerator(DataGenerator dataGenerator){
        dataGeneratorRepository.putDependenciesFor(dataGenerator);
    }

    private void checkAllDataGenerators(){
        for(DataGenerator dataGenerator: this.dataGeneratorRepository.getDependencies()){
            try {
                this.checkerRunner.checkDataGenerator(dataGenerator, this.dataGeneratorRepository);
            } catch(DataGeneratorCheckerException e){
                dataGeneratorRepository.removeDataGenerator(dataGenerator.getClass());
                throw new DataGeneratorException("An error occurred when check data generator class to add [" + dataGenerator.getClass() + "].",e);
            }
        }
    }

    /** Remove a data class generator from process to run.
     *
     * @param dataGeneratorClass to remove.
     * @return true if it removes data class generator. Otherwise it returns false.
     */
    public boolean removeDataGeneratorClass(Class<? extends DataGenerator> dataGeneratorClass){
        return dataGeneratorRepository.removeDataGenerator(dataGeneratorClass);
    }

    /** Test if it contains data class generator.
     *
     * @param dataGeneratorClass to test.
     * @return true if it contains data class generator. Otherwise it returns false.
     */
    public boolean containsDataGeneratorClass(Class<? extends DataGenerator> dataGeneratorClass){
        return dataGeneratorRepository.containsDataGeneratorClassAsGenerator(dataGeneratorClass);
    }

    /** Generate all data for all data generators added.
     *
     * @return an instances domain object's collection.
     */
    public <T> Collection<T> generateData(){
    	LOGGER.info("Pre process was executed: [" + this.getPreProcessExecuted() + "].");
    	long init = System.currentTimeMillis();
        this.checkAllDataGenerators();

        if(!this.getPreProcessExecuted()){
        	this.preProcess();
        }

        List<T> dataGenerated = this.createNewData();

        long end = System.currentTimeMillis();
        LOGGER.info("Pre process was executed: [" + this.getPreProcessExecuted() + "].");
        LOGGER.info("Data generator process was executed correctly in [" + (end - init) + "] milliseconds.");
        return dataGenerated;
    }

    /**
     * Pre process before run data generator process.
     *
     */
    public void preProcess() {
    	if(this.getPreProcessStrategy() != null){
    		LOGGER.info("Pre processing with strategy [" + this.getPreProcessStrategy().getName() + "].");
    		this.getPreProcessStrategy().initializePreProcess();
    		for(DataClassGenerator dataClassGenerator : this.dataGeneratorRepository.getInverseSortedDataClassGenerators()){
	            try {
	            	this.getPreProcessStrategy().preProcess(dataClassGenerator);
	            } catch (Exception e) {
	                throw new DataGeneratorException("An expected error occurred pre processing data generator [" + dataClassGenerator.getDataGeneratorClassSimpleName() + "].", e);
	            }
    		}
    		this.getPreProcessStrategy().finalizePreProcess();
    	} else {
    		LOGGER.info("Not pre processing strategy found...");
    	}
    	this.setPreProcessExecuted(Boolean.TRUE);
	}

	@SuppressWarnings("unchecked")
	protected <T> List<T> createNewData() {
        List<T> dataGenerated = new ArrayList<T>();
        try {
            for(DataClassGenerator dataClassGenerator : this.dataGeneratorRepository.getSortedDataClassGenerators()){
            	LOGGER.info("Generating data for [DataGeneratorClass: " + dataClassGenerator.getDataGeneratorClassSimpleName() + "- DomainObject: " + dataClassGenerator.getDomainObjectClassName() + "].");
                dataGenerated.addAll((Collection<? extends T>) dataClassGenerator.generateData(this.daoFactory));
            }
        } catch (Exception e) {
            throw new DataGeneratorException("An expected error occured generating new data generated. Verify data generator dependences or required data object. ", e);
        }
        return dataGenerated;
    }

    /** Generate all data for a data class generator. It and its dependences must be added as dependences.
     *
     * @param dataClassGeneratordata class generator to generate data.
     * @return an instances collection generated.
     */
    @SuppressWarnings("unchecked")
	public <T,H> Collection<H> generateDataFor(Class<T> dataClassGeneratorToGenerate) {
        this.checkAllDataGenerators();
        this.checkDataGenerator((Class<T>) dataClassGeneratorToGenerate);

        boolean terminated = false;
        List<H> dataGenerated = new ArrayList<H>();
        boolean containsDataGeneratorClassToGenerate = dataGeneratorRepository.containsDataGeneratorClassAsGenerator((Class<? extends DataGenerator>) dataClassGeneratorToGenerate);
        if(containsDataGeneratorClassToGenerate){
            this.preProcess();
            for(DataClassGenerator dataClassGenerator : this.dataGeneratorRepository.getSortedDataClassGenerators()){
                if(!terminated){
                    if(dataClassGenerator.isForDataGenerator((Class<? extends DataGenerator>) dataClassGeneratorToGenerate)){
                        dataGenerated.addAll((Collection<? extends H>) dataClassGenerator.generateData(this.daoFactory));
                        terminated = true;
                    } else {
                        dataClassGenerator.generateData(this.daoFactory);
                    }
                }
            }
        } else {
            throw new DataGeneratorException("Data generator class [" + dataClassGeneratorToGenerate.getCanonicalName() + "] wasn't added to data generator runner to generate its data.");
        }
        return dataGenerated;
    }

    /** Check if data class generator provided by user is null. If it is null it throw a DataGeneratorException.
     *
     * @param dataClassGeneratorToGenerate to check.
     */
    private <T> void checkDataGenerator(Class<T> dataClassGeneratorToGenerate) {
        if(dataClassGeneratorToGenerate == null){
            throw new DataGeneratorException("Data class generator provided by user mustn't be null.");
        }
    }

    /** Find all data generated for a data class generator.
     *
     * @param dataGeneratorClass to find its data generated.
     * @return an instances collection.
     */
    public Collection<Object> getDataGeneratedFor(Class<? extends DataGenerator> dataGeneratorClass) {
        DataClassGenerator dataClassGenerator = dataGeneratorRepository.getDataClassGeneratorFor(dataGeneratorClass);
        return dataClassGenerator.getDataGenerated();
    }

    /** Clear all data generators added recently.
     *
     */
    public void clearDataGenerators() {
        dataGeneratorRepository.clear();
    }

	/**
	 * @return the preProcessStrategy
	 */
	public PreProcessStrategy getPreProcessStrategy() {
		return preProcessStrategy;
	}

	/**
	 * @param preProcessStrategy the preProcessStrategy to set
	 */
	public void setPreProcessStrategy(PreProcessStrategy preProcessStrategy) {
		this.preProcessStrategy = preProcessStrategy;
	}

	/**
	 * @return the preProcessExecuted
	 */
	public Boolean getPreProcessExecuted() {
		return preProcessExecuted;
	}

	/**
	 * @param preProcessExecuted the preProcessExecuted to set
	 */
	protected void setPreProcessExecuted(Boolean preProcessExecuted) {
		this.preProcessExecuted = preProcessExecuted;
	}

	public List<? extends DataClassGenerator> prepareDependenciesOrdered() {
		return dataGeneratorRepository.getSortedDataClassGenerators();
	}

	private List<? extends DataGenerator> buildDataGeneratorList(List<? extends DataClassGenerator> dataClassGenerators){
		List<DataGenerator> dataGenerators = new ArrayList<DataGenerator>();
		for(DataClassGenerator a: dataClassGenerators){
			dataGenerators.add(a.getDataGenerator());
		}
		return dataGenerators;
	}

	@SuppressWarnings("unchecked")
	private List<Class<? extends DataGenerator>> buildDataGeneratorClassList(List<? extends DataGenerator> dataGeneratorList){
		List<Class<? extends DataGenerator>> dataGeneratorClassList = new ArrayList<Class<? extends DataGenerator>>();
		for(DataGenerator a: dataGeneratorList){
			dataGeneratorClassList.add((Class<? extends DataGenerator>) a.thisDataGeneratorClass());
		}
		return dataGeneratorClassList;
	}

	public <T extends DataGenerator> boolean areDependenciesBeforeThat(Class<T> dependence, Class<T>[] itsDependencies) {
		List<? extends DataClassGenerator> dataClassGeneratorList = this.prepareDependenciesOrdered();
		List<? extends DataGenerator> dataGeneratorList = this.buildDataGeneratorList(dataClassGeneratorList);
		List<Class<? extends DataGenerator>> dataGeneratorClassList = this.buildDataGeneratorClassList(dataGeneratorList);
		DataGeneratorResult result = new DataGeneratorResult(dataGeneratorClassList);
		return result.areDependenciesBeforeThat(dependence, itsDependencies);
	}

	public <T extends DataGenerator> List<Class<? extends DataGenerator>> dependenciesBeforeThat(Class<T> dependency) {
		List<? extends DataClassGenerator> dataClassGeneratorList = this.prepareDependenciesOrdered();
		List<? extends DataGenerator> dataGeneratorList = this.buildDataGeneratorList(dataClassGeneratorList);
		List<Class<? extends DataGenerator>> dataGeneratorClassList = this.buildDataGeneratorClassList(dataGeneratorList);
		DataGeneratorResult result = new DataGeneratorResult(dataGeneratorClassList);
		return result.dependenciesBeforeThat(dependency);
	}

	/**
	 * @param daoFactory the daoFactory to set
	 */
	public void setDaoFactory(DAOFactory daoFactory) {
		this.daoFactory = daoFactory;
	}

	public void finalizeGenerator() {
		this.clearDataGenerators();
	}

	/**
	 * @return the dataGeneratorRepository
	 */
	protected DataGeneratorRepository getDataGeneratorRepository() {
		return dataGeneratorRepository;
	}

	/**
	 * @param dataGeneratorRepository the dataGeneratorRepository to set
	 */
	protected void setDataGeneratorRepository(
			DataGeneratorRepository dataGeneratorRepository) {
		this.dataGeneratorRepository = dataGeneratorRepository;
	}
}
